﻿// -------------------------------------------------------------------------------------------------
// <copyright file="TVDBScraper.cs" company="HeterogeneousComputing.com">
// Creative Commons Attribution-ShareAlike 3.0 Unported License
// </copyright>
//
// <author>Jock</author>
//
// <date>04/08/2011</date>
//
// <summary>Implements the tvdb scraper class</summary>
// -------------------------------------------------------------------------------------------------
namespace TheTVDBScraper
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Xml;
    using System.Xml.Linq;
    using NLog;

    /// <summary>
    /// Main Class of the TheTVDB Scraper.
    /// </summary>
    /// <example>
    /// //// Create a TVDBScraper instance<br/>
    /// TVDBScraper scraper = new TVDBScraper();<br/><br/>
    /// //// Setup our return type of a List of TVSeries and Search using the<br/>
    /// ////   series name e.g. 24 and the language we require the series in e.g. English<br/>
    /// List&lt;TVSeries&gt; resultsList = scraper.Search("24", "English");<br/><br/>
    /// //// Process the first match which is ordered on relevance<br/>
    /// TVSeries tvseries = resultsList.ElementAt(0);<br/><br/>
    /// //// Obtain more detailed information for the show as an Object graph<br/>
    /// scraper.GetDetailedInformation(tvseries);<br/><br/>
    /// //// or obtain more detailed information for the show as XML<br/>
    /// scraper.GetDetailedXML(tvseries);<br/>
    /// </example>
    public class TVDBScraper : DataSet, INotifyPropertyChanged
    {
        #region Fields

            /// <summary>
            /// Api Key to acces tvdb.
            /// </summary>
            private const string ApiKey = "E0A58A99EF2075CA";

            /// <summary> Logging Framework. </summary>
            private static Logger logger = NLog.LogManager.GetCurrentClassLogger();

            /// <summary>
            /// Our HTTP Web client that will connect to
            /// TheTVDB website API.
            /// </summary>
            private WebClient webClient = new WebClient();

            /// <summary>
            /// TheTVDB Mirror server we will use for XML file
            /// lookups.
            /// </summary> 
            private TVDBSiteMirror xmlFileMirror;

            /// <summary>
            /// TheTVDB Mirror server we will use for Banner file
            /// lookups.
            /// </summary>
            private TVDBSiteMirror zipFileMirror;

            /// <summary>
            /// TheTVDB Mirror server we will use for ZIP file
            /// lookups.
            /// </summary>
            private TVDBSiteMirror bannerFileMirror;

            /// <summary>
            /// TheTVDB Mirror server time.
            /// </summary>
            private string previousServerTime;

            /// <summary> The tv series factory. A class that is responsible
            /// for constructing our object graph of TVSeries, TVSeason, TVEpisode etc. </summary>
            private TVSeriesFactory tvSeriesFactory = new TVSeriesFactory();

            /// <summary> Dictionary of xml files extracted from the ZIP for the TVSeries.<br/><br/>
            /// The ZIP should contain 3 files:-<br/>
            ///  language.xml where language is en etc.<br/>
            ///  banners.xml which contains links to the Banner Art for the TVSeries.<br/>
            ///  actors.xml which contains the Actors and the TVCharacter role they play
            ///  within the series.<br/>
            ///  </summary>
            private Dictionary<string, string> xmlFileDictionary = new Dictionary<string, string>();

            /// <summary> Event queue for all listeners interested in PropertyChanged events. </summary>
            public event PropertyChangedEventHandler PropertyChanged;

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Notifies a property changed. </summary>
            /// <remarks>    The method author was Jock, on the 09/08/2011. </remarks>
            /// <param name="info"> The information on what has changed. </param>
            /// -------------------------------------------------------------------------------------------------
            private void NotifyPropertyChanged(String info)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(info));
                }
            }

        #endregion

        #region Public Constructors
        #endregion

        #region Enums

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Values that represent MirrorsTypeMaskOptionsEnum.  </summary>
            /// <remarks>   Function Autor was Jock, on the 04/08/2011. </remarks>
            /// -------------------------------------------------------------------------------------------------
            public enum MirrorsTypeMaskOptionsEnum
            {
                /// <summary> NULL Means to options set.  </summary>
                NULL = 0x00,

                /// <summary> Mirror site supports sending XML summary
                /// data of TVShows.
                /// </summary>
                XML_FILES = 0x01,

                /// <summary> Mirror site supports sending Banner
                /// file information and URLs.
                /// </summary>
                BANNER_FILES = 0x02,

                /// <summary> Mirror site supports sending ZIP files
                /// containing detailed data of a TVShow.</summary>
                ZIP_FILES = 0x04
            }
        #endregion

        #region Public Operations
            /// <summary>
            /// Searches for a series by name on TheTVDB.
            /// Mirror servers are identified using a default mask
            /// of XML + Banner + ZIP.
            /// </summary>
            /// <param name="name"> Name of the TVSeries. </param>
            /// <param name="language"> Preferred language. </param>
            /// <returns> A list of TVSeries. </returns>
            public List<TVSeries> Search(string name, string language)
            {
                // Create a default search mask for mirrors looking for XML Files
                //   Banner Files and ZIP files.
                return Search(
                    name,
                    language,
                    new List<MirrorsTypeMaskOptionsEnum>
                    {
                        MirrorsTypeMaskOptionsEnum.XML_FILES,
                        MirrorsTypeMaskOptionsEnum.BANNER_FILES, 
                        MirrorsTypeMaskOptionsEnum.ZIP_FILES 
                    });
            }

            /// <summary>
            /// Searches for a series by name on TheTVDB.
            /// Mirror servers are identified using a user supplied mask.
            /// </summary>
            /// <param name="name"> Name of the series. </param>
            /// <param name="language"> Preferred language. </param>
            /// <param name="typemask"> A list of masks for which we are looking for
            /// mirror sites for. </param>
            /// <returns> A list of TVSeries. </returns>
            public List<TVSeries> Search(string name, string language, List<MirrorsTypeMaskOptionsEnum> typemask)
            {
                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Starting TVDBScraper.Search(\"{0}\", \"{1}\")", name, language);
                    }
                #endregion

                // Get back a list of all the mirrors
                List<TVDBSiteMirror> mirrorList = this.FindMirrors(typemask);
                this.SetXMLMirror(mirrorList);
                this.SetBannerMirror(mirrorList);
                this.SetZipMirror(mirrorList);
                this.SetServerTime();
                string link = this.xmlFileMirror.MirrorPath + "/api/GetSeries.php?seriesname=" + name + "&language=" + language;
                string resultingXml = this.webClient.DownloadString(link);

                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Finished TVDBScraper.Search()");
                    }
                #endregion

                return TVSeriesFactory.Parse(resultingXml);
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Gets detailed information for a particular TVSeries and returns the
            /// output as an object graph. </summary>
            /// <remarks>   The function author was Jock, on the 04/08/2011. </remarks>
            /// <param name="series">   The series for which to retrieve data. </param>
            /// -------------------------------------------------------------------------------------------------
            public void GetDetailedInformation(TVSeries series)
            {
                Dictionary<string, string> xmlFileDictionary = this.DownloadAndExtractZIP(series);
                this.tvSeriesFactory.ParseDetail(series, xmlFileDictionary);
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary> Gets detailed information for a particular TVSeries and returns
            /// the output in XML format. This call delegates to
            /// <see cref="GetDetailedInformation"/>
            /// to do the work and then transforms the
            /// object graph to XML.</summary>
            /// <remarks>    The method author was Jock, on the 05/08/2011. </remarks>
            /// <param name="series">   The series for which to retrieve data. </param>
            /// <returns>   The base xml. </returns>
            /// -------------------------------------------------------------------------------------------------
            public string GetDetailedXML(TVSeries series)
            {
                #region Logging
                if (logger.IsDebugEnabled)
                {
                    logger.Debug("Entering TVDBScraper.GetDetailedXML(\"{0}\")", series.SeriesName);
                }
                #endregion
                string outputXML;
                this.xmlFileDictionary = this.DownloadAndExtractZIP(series);
                outputXML = this.tvSeriesFactory.ParseDetailXML(series, this.xmlFileDictionary);

                #region Logging
                if (logger.IsDebugEnabled)
                {
                    logger.Debug("Exiting TVDBScraper.GetDetailedXML() -> OutputXML");
                    logger.Debug(outputXML);
                }
                #endregion

                return outputXML;
            }
           
        #endregion 

        #region Private Operations

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Downloads the and extracts the ZIP for the TVSeries. </summary>
            /// <remarks>    The method author was Jock, on the 05/08/2011. </remarks>
            /// <param name="series">   The series for which to retrieve data. </param>
            /// <returns>   Returns a string for each of the three files parsed.
            /// Each string contains the XML data. </returns>
            /// -------------------------------------------------------------------------------------------------
            private Dictionary<string, string> DownloadAndExtractZIP(TVSeries series)
            {
                #region Logging
                if (logger.IsDebugEnabled)
                {
                    logger.Debug("Entering TVDBScraper.DownloadAndExtractZIP(\"{0}\")", series.SeriesName);
                }
                #endregion

                string link = this.zipFileMirror.MirrorPath + "/api/" + ApiKey + "/series/" + series.SeriesId + "/all/" + series.Language + ".zip";

                if (logger.IsDebugEnabled)
                {
                    logger.Debug("TVDBScraper.DownloadAndExtractZIP Downloading : {0}", link);
                }

                this.webClient.DownloadFile(link, series.SeriesId + "_" + series.Language + ".zip");
                ZipFileHandler zfh = new ZipFileHandler();
                Dictionary<string, string> xmlFileDictionary =
                    zfh.OpenZipFile(series.SeriesId + "_" + series.Language + ".zip");

                #region Logging
                if (logger.IsDebugEnabled)
                {
                    logger.Debug("Exiting TVDBScraper.DownloadAndExtractZIP()");
                }
                #endregion

                return xmlFileDictionary;
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Searches for the mirror sites for TheTVDB. </summary>
            /// <remarks>   The function author was Jock, on the 04/08/2011. </remarks>
            /// <param name="typemask"> A list of masks for which we are looking for mirror sites for. </param>
            /// <returns>   The mirror sites found. </returns>
            /// -------------------------------------------------------------------------------------------------
            private List<TVDBSiteMirror> FindMirrors(List<MirrorsTypeMaskOptionsEnum> typemask)
            {
                string mirrorsRequest = "http://www.thetvdb.com/api/" + ApiKey + "/mirrors.xml";
                string resultingXml = this.webClient.DownloadString(mirrorsRequest);
                return TVDBSiteMirrorFactory.Parse(resultingXml);
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Sets a mirror site to retrieve XML TVSeries summary information from. </summary>
            /// <remarks>   The function author was Jock, on the 04/08/2011. </remarks>
            /// <param name="mirrorList">   List of previosuly obtained mirror sites. </param>
            /// -------------------------------------------------------------------------------------------------
            private void SetXMLMirror(List<TVDBSiteMirror> mirrorList)
            {
                IEnumerable<TVDBSiteMirror> mirrorQuery =
                   from mirror in mirrorList
                   where mirror.SupportsXML
                   select mirror;

                // Randomise the mirror we used based on how many fit our criteria
                Random rand = new Random();
                this.xmlFileMirror = mirrorQuery.ElementAt(rand.Next(mirrorQuery.Count()));
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Sets a mirror site to retrieve Banner information from. </summary>
            /// <remarks>   The function author was Jock, on the 04/08/2011. </remarks>
            /// <param name="mirrorList">   List of previosuly obtained mirror sites. </param>
            /// -------------------------------------------------------------------------------------------------
            private void SetBannerMirror(List<TVDBSiteMirror> mirrorList)
            {
                IEnumerable<TVDBSiteMirror> mirrorQuery =
                   from mirror in mirrorList
                   where mirror.SupportsBanner
                   select mirror;
            
                // Randomise the mirror we used based on how many fit our criteria
                Random rand = new Random();
                this.bannerFileMirror = mirrorQuery.ElementAt(rand.Next(mirrorQuery.Count()));
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Sets a TheTVDB Mirror Site from which to download TVShow zip files. </summary>
            /// <remarks>   The function author was Jock, on the 04/08/2011. </remarks>
            /// <param name="mirrorList">   List of previosuly obtained mirror sites. </param>
            /// -------------------------------------------------------------------------------------------------
            private void SetZipMirror(List<TVDBSiteMirror> mirrorList)
            {
                IEnumerable<TVDBSiteMirror> mirrorQuery =
                   from mirror in mirrorList
                   where mirror.SupportsZIP
                   select mirror;
            
                // Randomise the mirror we used based on how many fit our criteria
                Random rand = new Random();
                this.zipFileMirror = mirrorQuery.ElementAt(rand.Next(mirrorQuery.Count()));
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Sets the server time. </summary>
            /// <remarks>   The function author was Jock, on the 04/08/2011. </remarks>
            /// -------------------------------------------------------------------------------------------------
            private void SetServerTime()
            {
                string link = this.xmlFileMirror.MirrorPath + "/api/Updates.php?type=none";
                string resultingXml = this.webClient.DownloadString(link);
                XElement resultsDoc = XElement.Load(new StringReader(resultingXml));

                IEnumerable<XElement> serverItems =
                    from item in resultsDoc.Descendants("Time")
                    select item;
                this.previousServerTime = (string)serverItems.ElementAt(0) ?? null;
            }
        #endregion
    }
}

/// -------------------------------------------------------------------------------------------------
// End of TVDBScraper.cs
/// -------------------------------------------------------------------------------------------------
